Skip to content

Comments

feat(Tabs): add native RTL support for bottom tabs on iOS & Android#3613

Open
ahmedawaad1804 wants to merge 13 commits intosoftware-mansion:mainfrom
ahmedawaad1804:add-bottomtab-rtl-support
Open

feat(Tabs): add native RTL support for bottom tabs on iOS & Android#3613
ahmedawaad1804 wants to merge 13 commits intosoftware-mansion:mainfrom
ahmedawaad1804:add-bottomtab-rtl-support

Conversation

@ahmedawaad1804
Copy link

@ahmedawaad1804 ahmedawaad1804 commented Feb 3, 2026

Description

This PR adds proper RTL (Right-to-Left) layout support for native bottom tabs on both iOS and Android in react-native-screens.

Previously, bottom tabs did not automatically follow the system RTL direction, which caused incorrect tab ordering and layout in RTL locales (e.g. Arabic, Hebrew).

With this change bottom tabs now follow the system layout direction by default. This brings native behavior in line with expected platform RTL handling and React Native layout conventions.

Details

@kligarski:

Android

On Android, the direction works out-of-the-box so no custom logic is necessary.

iOS

Badges

Setting semanticContentAttribute for _controller.tabBar and _controller.view is not enough, e.g. badges visible through liquid glass lens are still in LTR.

Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.16.27.18.mov

To handle this, we're using the same approach as in native stack - we set UIView's appearanceWhenContainedInInstancesOfClasses of the tab bar (details how it's handled in the header are here).

However, this does not work for tab bar & sidebar on iPad starting from iOS 18 as it is not a part of controller.tabBar. _UITabContentView is mounted under controller.view. Using appearanceWhenContainedInInstancesOfClasses for _UITabContentView (which is already sketchy as this is an internal UIKit class) helps with the order of items in the tab bar but the sidebar appears on the wrong side of the screen.

That's why I decided to use modern way to handle direction via trait overrides. For iOS prior to 17, you need to apply overrides on parent view controller (see here), that's why I decided to leave current solution with appearanceWhenContainedInInstancesOfClasses for those versions instead of trait override. There is no iPad top tab bar/sidebar/bottom accessory in these versions of iOS so the solution should be enough.

For iOS 17+, we can use traitOverrides directly on the controller - this works with the top tab bar/sidebar on iPadOS 18+.

ScrollView

On iOS, there seems to be a very odd bug with content of the ScrollView being moved off screen after tab changes. I've created an issue on our internal board (https://github.com/software-mansion/react-native-screens-labs/issues/985) and we'll investigate this further.

Screen.Recording.2026-02-19.at.15.55.06.mov
Bottom Accessory

There seems to be a bug with bottom accessory in RTL when search role is NOT used for one of the tabs (Apple Music and Apple Podcasts use search role so the bug isn't visible).

no search role (bug) search role (no bug)
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.52.15.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.51.31.mov

This bug is reproducible in bare UIKit app on iOS 26.2.

Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.16.19.15.mov

I've added this to our internal board (https://github.com/software-mansion/react-native-screens-labs/issues/986) and we'll check whether it has been fixed in iOS 26.3/26.4 beta.

Top tab bar badges

On iOS 18+, there seems to be a bug with the initial position of the badges. They move to correct position after a tab change. I added a ticket on our internal board to check whether this is a native bug: https://github.com/software-mansion/react-native-screens-labs/issues/991.

Simulator.Screen.Recording.-.iPad.Pro.13-inch.M5.-.2026-02-23.at.09.47.57.mov

Changes

  • Added automatic RTL detection in native bottom tabs (iOS & Android)
  • Synced tab layout direction with system layout direction by default
  • Updated native layout logic to correctly reverse tab ordering in RTL mode
  • Added example in Test3598.tsx

Before

(iOS only because Android works out of the box).
Simulator Screenshot - iPhone 17 Pro Max - 2026-02-03 at 18 39 51

After

Android

Android Test3598
android_3613.mp4

iOS

iOS Test3598 iOS TestBottomTabs iOS Test3288
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.49.45.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.50.11.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.51.31.mov

Test plan

Use Test3598, TestBottomTabs, Test3288 (iOS).

Tested on:

  • Android (RTL system language enabled)
  • iOS (RTL simulator + device)

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • Ensured that CI passes

@kkafar
Copy link
Member

kkafar commented Feb 16, 2026

Hey, thanks for the PR. This is great, we need it. I hope to review it somewhere this week. Will keep you up to date.

@kligarski kligarski self-assigned this Feb 17, 2026
@kligarski
Copy link
Contributor

Hi, thank you for the PR. I'll push some changes and update PR description so we can land it soon. I hope you don't mind.

@ahmedawaad1804
Copy link
Author

@kligarski sure thank you :D

@ahmedawaad1804
Copy link
Author

@kkafar welcome , any time :)

Copy link
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving some comments for the reviewers.

}

if (newComponentProps.directionMode != oldComponentProps.directionMode) {
_directionMode =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use [RCTI18nUtil isRTL] instead of passing a prop, it should have an updated value as it reads it from [NSUserDefaults standardUserDefaults] (but it would be unable to react to dynamic changes). I'm not sure if we can consider this a stable API but it has been mentioned in blog post in react-native: https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps.

cc @kkafar - let me know what you think is better

We can also leave the prop for now (it's not exposed as a part of API either way) and rethink our approach to RTL in separate PR - I'm wondering whether RTL should be handled via some top-level wrapper component for the entire hierarchy below it.

Copy link
Contributor

@satya164 satya164 Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kligarski in react navigation, we have a direction prop on NavigationContainer to control layout direction which we will pass it to navigators that can handle it.

@kligarski kligarski changed the title Add native RTL support for bottom tabs on iOS & Android with optional direction override feat(Tabs): add native RTL support for bottom tabs on iOS & Android Feb 23, 2026
Copy link
Contributor

@t0maboro t0maboro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving 1 comment, I haven't verified the runtime yet

Comment on lines +568 to +581
#if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (@available(iOS 17.0, *)) {
_controller.traitOverrides.layoutDirection = _directionMode == UISemanticContentAttributeForceRightToLeft
? UITraitEnvironmentLayoutDirectionRightToLeft
: UITraitEnvironmentLayoutDirectionLeftToRight;
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
{
_controller.view.semanticContentAttribute = _directionMode;
_controller.tabBar.semanticContentAttribute = _directionMode;
[[UIView appearanceWhenContainedInInstancesOfClasses:@[ _controller.tabBar.class ]]
setSemanticContentAttribute:_directionMode];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeated from updateProps, can be deduplicated with a helper method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants